# -*- coding: utf-8 -*-
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.autograd import Variable
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
import os, shutil

#
from config import config as cfg
from models.vgg import vgg

torch.manual_seed(cfg.manualseed)
torch.cuda.manual_seed(cfg.manualseed)

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# additional subgradient descent on the sparsity-induced penalty term
def updateBN(model, cfg):
    for m in model.modules():
        if isinstance(m, nn.BatchNorm2d):
            m.weight.grad.data.add_(cfg.s*torch.sign(m.weight.data))  # L1

# 训练
def train(model, criterion, optimizer, train_loader, epoch, cfg):
    model.train()
    avg_loss = 0.
    train_cc = 0.
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)
        data, target = Variable(data), Variable(target)

        optimizer.zero_grad()
        output = model(data)
        loss = criterion(output, target)
        avg_loss += loss.item()
        pred = output.data.max(1, keepdim=True)[1]
        train_cc += pred.eq(target.data.view_as(pred)).cpu().sum()
        loss.backward()
        if cfg.sr:
            updateBN(model, cfg) # L1
        optimizer.step()
        if batch_idx % cfg.log_interval == 0:
            print('Train Epoch: {} [{}/{} ({:.1f}%)]\tLoss: {:.6f}'.format(
                epoch, batch_idx * len(data), len(train_loader.dataset),
                100. * batch_idx / len(train_loader), loss.item()))



def test(model, criterion, test_loader):
    model.eval()
    test_loss = 0.
    correct = 0.
    for data, target in test_loader:
        data, target = data.to(device), target.to(device)
        data, target = Variable(data), Variable(target)

        with torch.no_grad():
            output = model(data)
        test_loss += criterion(output, target).item()
        pred = output.data.max(1, keepdim=True)[1]
        correct += pred.eq(target.data.view_as(pred)).cpu().sum()

    test_loss /= len(test_loader.dataset)
    print('Test set: Average loss: {:.4f}, Accuracy: {}/{} ({:.1f}%)'.format(
        test_loss, correct, len(test_loader.dataset),
        100. * correct / len(test_loader.dataset)))
    return (correct / float(len(test_loader.dataset))).item()



if __name__ == '__main__':
    # 保存
    if not os.path.exists(cfg.checkout):
        os.mkdir(cfg.checkout)

    # 数据集
    # 训练集
    train_transform = transforms.Compose([
            transforms.Pad(4),
            transforms.RandomCrop(32),
            transforms.RandomHorizontalFlip(),
            transforms.ToTensor(),
            transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
        ])
    train_set = datasets.CIFAR10("data", train=True, download=True, transform=train_transform)
    train_loader = DataLoader(train_set, batch_size=cfg.batch_size, num_workers=cfg.num_workers,
                              shuffle=True, drop_last=True, pin_memory=True)
    # 测试集合
    test_transform = transforms.Compose([
            transforms.ToTensor(),
            transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
        ])
    test_set = datasets.CIFAR10("data", train=False, download=True, transform=test_transform)
    test_loader = DataLoader(test_set, batch_size=cfg.batch_size, num_workers=cfg.num_workers,
                             shuffle=False, drop_last=True, pin_memory=True)

    # 开始的epoch
    start_epoch = cfg.start_epoch

    # 网络

    # 加载减枝模型
    if cfg.prune_model != "":
        print("load prune model: {}".format(cfg.prune_model))
        checkout = torch.load(cfg.prune_model)
        model = vgg(cfg=checkout["cfg"])
        model.load_state_dict(checkout["checkout"])
        model.to(device) # cpu -> gpu

    # 损失函数
    criterion = nn.CrossEntropyLoss()

    # 优化器
    optimizer = optim.SGD(model.parameters(), lr=cfg.lr, momentum=cfg.momentum, weight_decay=cfg.weight_decay)

    # 训练
    for epoch in range(start_epoch, cfg.num_epochs + 1):
        # 学习率衰减
        if epoch in [cfg.num_epochs * 0.5, cfg.num_epochs * 0.75]: # [80,120]
            for param_group in optimizer.param_groups:
                param_group['lr'] *= 0.1
        # 训练集
        train(model, criterion, optimizer, train_loader, epoch, cfg)
        # 测试集
        correct = test(model, criterion, test_loader)
        # 保存模型
        if correct > cfg.is_best: # 准确率更新
            cfg.is_best = correct
            torch.save(model.state_dict(), cfg.prune_finetune)
            shutil.copyfile(cfg.prune_finetune, f"{cfg.prune_finetune}.bak")

    print("best correct is:", cfg.is_best)
